package com.cea.papyrus.core.actions;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.gef.Request;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CompoundCommand;
import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.NamedElement;
import org.eclipse.uml2.uml.Stereotype;

import com.cea.papyrus.core.PapyrusPlugin;
import com.cea.papyrus.core.editpart.GraphEdgeEditPart;
import com.cea.papyrus.core.editpart.GraphNodeEditPart;
import com.cea.papyrus.core.editpart.IUMLElementEditPart;
import com.cea.papyrus.core.editpart.IUMLObjectEditPart;
import com.cea.papyrus.core.helper.ModelerHelper;
import com.cea.papyrus.core.ui.dialogs.ApplyStereotypeSelectionDialog;
import com.cea.papyrus.core.utils.Debug;
import com.cea.papyrus.diagraminterchange2.di2.Di2Factory;
import com.cea.papyrus.diagraminterchange2.di2.DiagramElement;
import com.cea.papyrus.profile.ui.dialogs.ChooseSetStereotypeDialog;

public class ApplyStereotypeAction extends org.eclipse.gef.ui.actions.SelectionAction {

	/** debug channel */
	protected final int channel = 21;

	/** request key for the request associated to this action  */
	public final static String APPLY_STEREOTYPE_ACTION_REQUEST = ApplyStereotypeAction.class.getName()+ "Request";

	/** ID of  this action */
	public final static String APPLY_STEREOTYPE_ACTION_ID = ApplyStereotypeAction.class.getName()+ "ID";

	/** Request created by this action */
	protected Request request;

	/**
	 * Creates a new ApplyStereotypeAction
	 * @param part that part which contains the selection
	 */public ApplyStereotypeAction(IWorkbenchPart part) {
		 super(part);
		 request = new Request(APPLY_STEREOTYPE_ACTION_REQUEST);
		 setText("Apply Stereotype");
		 setId(APPLY_STEREOTYPE_ACTION_ID);
		 setToolTipText("Apply Steretoype(s) to Element(s)");

		 // icon for action
		 ImageDescriptor descriptor = PapyrusPlugin.getImageDescriptor(PapyrusPlugin.UML_ICONS_16x16+"Stereotype.gif");
		 setImageDescriptor(descriptor);
		 setHoverImageDescriptor(descriptor);

	 }

	 /* (non-Javadoc)
	  * @see org.eclipse.gef.ui.actions.WorkbenchPartAction#calculateEnabled()
	  */
	 @Override
	 protected boolean calculateEnabled() {
		 return canPerformAction();
	 }	

	 /**
	  * Calculates if this action is enabled or not
	  * @return <code>true</code> if the action is enable
	  */
	 private boolean canPerformAction() {
		 // no element selected: can not perform action
		 if (getSelectedObjects().isEmpty()) {
			 return false;
		 }

		 boolean canPerform = true;
		 EClass eClass = null;

		 List<Object> parts = getSelectedObjects();
		 for (int i=0; i<parts.size(); i++){
			 Object o = parts.get(i);

			 // every object should be at least a uml object edit part 
			 if (!(o instanceof IUMLObjectEditPart)) {
				 canPerform = false;
			 } else {
				 // if this is a IUMLElement edit part
				 if(o instanceof IUMLElementEditPart) {

					 // check the managed element is not an imported element
					 if(((IUMLElementEditPart)o).isElementImport()) {
						 canPerform = false;
					 }

					 // this is a uml element, and not an imported element
					 Element e = ((IUMLElementEditPart)o).getUmlElement();
					 if(e==null) {
						 canPerform = false;
					 } else if(eClass == null) {
					 // eClass is null: first time we meet a uml element
						 eClass = e.eClass();
					 } else {
						 // eClass is not null: we already met a uml element. The type of e should be the same metaclass
						 if(!eClass.equals(e.eClass())) {
							 // not the same metaclass: return false
							 canPerform = false;
						 }
					 }
				 } else {
					 canPerform = false;
				 }
			 }
		 }
		 return canPerform;
	 }

	 /* (non-Javadoc)
	  * @see org.eclipse.jface.action.Action#run()
	  */
	 @Override
	 public void run () {
		 // open a dialog window for stereotype selection
		 ApplyStereotypeSelectionDialog dialog = new ApplyStereotypeSelectionDialog(this.getWorkbenchPart().getSite().getShell(), getReferenceElement());
		 int result = dialog.open();

		 if(result == ChooseSetStereotypeDialog.OK) {
			 final ArrayList stereotypesToApply = dialog.getSelectedElements();

			 BusyIndicator.showWhile(Display.getCurrent(), new Runnable() {
				 public void run() {
					 CompoundCommand command = new CompoundCommand();

					 List editparts = getSelectedObjects();
					 Debug.debug(this,"Action::getCommand()"+editparts.size(), 21);
					 for (int i=0; i < editparts.size(); i++) {
						 if(editparts.get(i) instanceof IUMLElementEditPart) {
							 IUMLElementEditPart part = (IUMLElementEditPart) editparts.get(i);
							 // creates a commmand that applies the steretoype to the current IUMLElementEditPart.Element
							 // 1. check this in not an imported element
							 if(!part.isElementImport()) {
								 // if associated uml element is not null
								 if(part.getUmlElement() !=null) {
									 Element element = part.getUmlElement();

									 command.add(new ApplyStereotypeCommand(part, stereotypesToApply, true));
								 }
							 }
						 }
					 }
					 execute(command);
				 }
			 });

		 }
	 }

	 /* (non-Javadoc)
	 * @see org.eclipse.gef.ui.actions.SelectionAction#setSelection(org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public void setSelection(ISelection selection) {
		// overrides for visibility
		super.setSelection(selection);
	}

	 /**
	  * Returns the first relevant element for selected objects,
	  * ie. the first element linked to a umlelement,
	  * but not an imported element.  
	  * @return the first relevant element, or <code>null</code> if
	  * no element was found
	  */
	 protected Element getReferenceElement() {
		 List<Object> parts = getSelectedObjects();
		 for (int i=0; i<parts.size(); i++){
			 Object o = parts.get(i);

			 // object should be at least a uml object edit part 
			 if(o instanceof IUMLElementEditPart) {

				 // check the managed element is not an imported element
				 if(!((IUMLElementEditPart)o).isElementImport()) {
					 return ((IUMLElementEditPart)o).getUmlElement();
				 }
			 }
		 }
		 return null;
	 }


	 /**
	  * Command that applies a stereotype to a uml element
	  */
	 public class ApplyStereotypeCommand extends Command {

		 /** Edit Part on which stereotype must be applied */
		 protected IUMLElementEditPart editPart;

		 /** UML element linked to the edit part */
		 protected Element element;

		 /** List of stereotypes to be applied */
		 protected ArrayList<Stereotype> stereotypesToApply;

		 /** List of stereotypes really applied. (removed already applied stereotypes from  stereotypesToApply */
		 protected BasicEList<Stereotype> appliedStereotypes;

		 /** boolean that indicates if new Steretoypes should be displayed */
		 protected Boolean graphicalDisplay;

		 /**
		  * Creates a new command that applies a list of steretoypes to the uml element
		  * linked to the specified IUMLElementEditPart.
		  * @param editPart the edit part that controls the uml element to be modified
		  * @param stereotypesToApply the list of stereotypes to apply
		  * @param graphicalDisplay <code>true</code> if the stereotypes have to be displayed, in case of a graphical edit part
		  */
		 public ApplyStereotypeCommand(IUMLElementEditPart editPart, ArrayList<Stereotype> stereotypesToApply, boolean graphicalDisplay) {
			 super();
			 this.editPart = editPart;
			 this.element = editPart.getUmlElement();
			 this.setLabel("Apply stereotypes to "+ ((element instanceof NamedElement)? ((NamedElement)element).getName() : element.eClass().getName()));
			 this.stereotypesToApply = stereotypesToApply;
			 this.graphicalDisplay = graphicalDisplay;
		 }

		 /* (non-Javadoc)
		  * @see org.eclipse.gef.commands.Command#execute()
		  */
		 @Override
		 public void execute() {
			 EList<Stereotype> oldStereotypeList = element.getAppliedStereotypes();

			 // Keep newStereotype order (will be used at the end of the method)
			 appliedStereotypes = new BasicEList<Stereotype>();
			 appliedStereotypes.addAll(stereotypesToApply);

			 // If the 2 lists differ, apply the new list of stereotypes
			 if(!(stereotypesToApply.equals(oldStereotypeList))) {

				 // Parse old list :
				 // if stereotype is in the new list : it is already applied
				 // --> don't unapply it
				 // --> remove it from new list
				 Iterator<Stereotype> it = oldStereotypeList.iterator();
				 while(it.hasNext()) {
					 Stereotype currentStOld = (Stereotype) it.next();
					 if (appliedStereotypes.contains(currentStOld)) {
						 appliedStereotypes.remove(currentStOld);
					 }
				 }

				 // Already applied stereotype should have been removed
				 // apply others
				 Iterator<Stereotype> newApplyStereotypes = appliedStereotypes.iterator();
				 while(newApplyStereotypes.hasNext()) {
					 Stereotype currentStereotype = (Stereotype) newApplyStereotypes.next();
					 element.applyStereotype(currentStereotype);

					 if(graphicalDisplay) {
						 addStereotypeDisplay(currentStereotype);
					 }

				 }

				 // Update Stereotype order
				 if(graphicalDisplay) {
					 // build stereotype display
					 buildStereotypeDisplay(element.getAppliedStereotypes());
				 }
			 }

		 }

		 /* (non-Javadoc)
		  * @see org.eclipse.gef.commands.Command#undo()
		  */
		 @Override
		 public void undo() {
			 // remove applied stereotypes in execute method
			 Iterator<Stereotype> it = appliedStereotypes.iterator();
			 while(it.hasNext()) {
				 Stereotype removedSt = it.next(); 
				 element.unapplyStereotype(removedSt);

				 if(graphicalDisplay) {
					 removeStereotypeDisplay(removedSt);
				 }
			 }

			 // Update Stereotype order
			 if(graphicalDisplay) {
				 // build stereotype display
				 buildStereotypeDisplay(element.getAppliedStereotypes());
			 }
		 }

		 /* (non-Javadoc)
		  * @see org.eclipse.gef.commands.Command#redo()
		  */
		 @Override
		 public void redo() {
			 execute();
		 }

		 /**
		  * Add a stereotype to the list of stereotypes to display.
		  * @param st the stereotype to add
		  */
		 protected void addStereotypeDisplay(Stereotype st) {
			 // bugfix: a selected element is not necessary a diagram element (ex: selection in the outline)
			 if(getDiagramElement() == null) {
				 return;
			 }

			 // property to add creation
			 com.cea.papyrus.diagraminterchange2.di2.Property property = Di2Factory.eINSTANCE.createProperty();
			 property.setKey(ModelerHelper.PROPERTY_STEREOTYPE_DISPLAY);
			 property.setValue(st.getQualifiedName());

			 // add the property
			 getDiagramElement().getProperty().add(property);
		 }

		 /**
		  * Remove the stereotype from the list of stereotypes to display.
		  * @param st the stereotype to remove
		  */
		 protected void removeStereotypeDisplay(Stereotype st) {
			 // bugfix: a selected element is not necessary a diagram element (ex: selection in the outline)
			 if(getDiagramElement() == null) {
				 return;
			 }

			 EList<com.cea.papyrus.diagraminterchange2.di2.Property> propList = getDiagramElement().getProperty();

			 // clone the list, because it will be modified
			 ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property> tempPropList = new ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property>();
			 tempPropList.addAll(propList);

			 // check if the property is a stereotype display property
			 // it it is one, it checks if it corresponds to the stereotype to be removed
			 Iterator<com.cea.papyrus.diagraminterchange2.di2.Property> iter = tempPropList.iterator();
			 while(iter.hasNext()) {
				 com.cea.papyrus.diagraminterchange2.di2.Property prop = (com.cea.papyrus.diagraminterchange2.di2.Property)iter.next();
				 if(prop.getKey().equals(ModelerHelper.PROPERTY_STEREOTYPE_DISPLAY)) {
					 if(prop.getValue().equals(st.getQualifiedName())) {
						 propList.remove(prop);
					 }
				 }
			 }
		 }

		 /**
		  * Returns the diagram element controlled by the edit Part
		  * @return the diagram element controlled by the edit Part, or <code>null</code>.
		  */
		 protected DiagramElement getDiagramElement() {
			 if((editPart instanceof GraphNodeEditPart) || (editPart instanceof GraphEdgeEditPart)) {
				 return (DiagramElement)((AbstractGraphicalEditPart)editPart).getModel();
			 }
			 return null;
		 }

		 /**
		  * This method builds the stereotypes to display list in order
		  * to reorder stereotypes display order according to stereotypes
		  * application order.
		  * @param appliedStereotypes the applied stereotypes
		  */
		 protected void buildStereotypeDisplay(EList<Stereotype> appliedStereotypes) {
			 // bugfix: a selected element is not necessary a diagram element (ex: selection in the outline)
			 if(getDiagramElement() == null) {
				 return;
			 }
			 // list containing stereotype display properties of the current graphElement
			 ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property> stereoDiplayList = new ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property>();

			 EList<com.cea.papyrus.diagraminterchange2.di2.Property> propList = getDiagramElement().getProperty();
			 // copy propList to tempPropList
			 ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property> tempPropList = new ArrayList<com.cea.papyrus.diagraminterchange2.di2.Property>();
			 tempPropList.addAll(propList);

			 // build stereoDiplayList and remove stereotype display properties form propList
			 Iterator<com.cea.papyrus.diagraminterchange2.di2.Property> iter = tempPropList.iterator();
			 while(iter.hasNext()){
				 com.cea.papyrus.diagraminterchange2.di2.Property prop = iter.next();
				 if(prop.getKey().equals(ModelerHelper.PROPERTY_STEREOTYPE_DISPLAY)) {
					 stereoDiplayList.add(prop);
					 propList.remove(prop);
				 }
			 }

			 // add stereotype display properties in the appropriate order
			 Iterator<Stereotype> stereoIter = appliedStereotypes.iterator();
			 while(stereoIter.hasNext()) {
				 String stereoName = stereoIter.next().getQualifiedName();
				 Iterator<com.cea.papyrus.diagraminterchange2.di2.Property> propIter = stereoDiplayList.iterator();
				 while(propIter.hasNext()) {
					 com.cea.papyrus.diagraminterchange2.di2.Property prop = propIter.next();
					 if(prop.getValue().equals(stereoName)) {
						 propList.add(prop);
					 }
				 }
			 }
		 }


	 }

}


